//C- -------------------------------------------------------------------
//C- Java DjVu (r) (v. 0.8)
//C- Copyright (c) 2004-2005 LizardTech, Inc.  All Rights Reserved.
//C- Java DjVu is protected by U.S. Pat. No.C- 6,058,214 and patents
//C- pending.
//C-
//C- This software is subject to, and may be distributed under, the
//C- GNU General Public License, Version 2. The license should have
//C- accompanied the software or you may obtain a copy of the license
//C- from the Free Software Foundation at http://www.fsf.org .
//C-
//C- The computer code originally released by LizardTech under this
//C- license and unmodified by other parties is deemed "the LIZARDTECH
//C- ORIGINAL CODE."  Subject to any third party intellectual property
//C- claims, LizardTech grants recipient a worldwide, royalty-free,
//C- non-exclusive license to make, use, sell, or otherwise dispose of
//C- the LIZARDTECH ORIGINAL CODE or of programs derived from the
//C- LIZARDTECH ORIGINAL CODE in compliance with the terms of the GNU
//C- General Public License.   This grant only confers the right to
//C- infringe patent claims underlying the LIZARDTECH ORIGINAL CODE to
//C- the extent such infringement is reasonably necessary to enable
//C- recipient to make, have made, practice, sell, or otherwise dispose
//C- of the LIZARDTECH ORIGINAL CODE (or portions thereof) and not to
//C- any greater extent that may be necessary to utilize further
//C- modifications or combinations.
//C-
//C- The LIZARDTECH ORIGINAL CODE is provided "AS IS" WITHOUT WARRANTY
//C- OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
//C- TO ANY WARRANTY OF NON-INFRINGEMENT, OR ANY IMPLIED WARRANTY OF
//C- MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
//C-
//C- In addition, as a special exception, LizardTech Inc. gives permission
//C- to link the code of this program with the proprietary Java
//C- implementation provided by Sun (or other vendors as well), and
//C- distribute linked combinations including the two. You must obey the
//C- GNU General Public License in all respects for all of the code used
//C- other than the proprietary Java implementation. If you modify this
//C- file, you may extend this exception to your version of the file, but
//C- you are not obligated to do so. If you do not wish to do so, delete
//C- this exception statement from your version.
//C- -------------------------------------------------------------------
//C- Developed by Bill C. Riemers, Foxtrot Technologies Inc. as work for
//C- hire under US copyright laws.
//C- -------------------------------------------------------------------
//
package com.lizardtech.djvu{
//import java.beans.*;
import flash.utils.ByteArray;
import flash.utils.getQualifiedClassName;

import java.io.*;
import java.net.URL;
import java.util.*;


/**
 * <p>
 * DjVuPage decodes single page DjVu files.  This class is appropriate for
 * decoding a single page.  If an INCL chunk is contained in the page, then
 * either decoding must be done from a URL or createDataPool must be
 * overloaded as in the case of the Document class.  The general usage of
 * this class is to create an instance, set any desired values, and then
 * call decode with the relevant data. Decoding may be done either
 * asynchronously or synchronously.  By default decoding is synchronous, but
 * this may be changed by calling setAsync(true) before calling decode. If
 * you will be accessing this class from another thread while decoding is in
 * progress  then you should use asynchronous mode.
 * </p>
 * 
 * <p>
 * In synchronous mode, the decode method will not return until decoding  is
 * complete.  In asynchronous mode decode returns right away as decoding
 * continues in a separate thread.  One method to track asynchronous decode
 * progress is to register a PropertyChangeListener.  Each codec will
 * generate a signal when created.  If you need to wait for a particular
 * codec, rather than polling, use the waitForCodec method.
 * </p>
 * 
 * <p>
 * The updateMap method should be used for progressive rendering. The
 * following example shows how to progressively update an image map in a
 * separate thread.
 * </p>
 * <pre>
 * class Progressive extends Thread
 * {
 *   GMap map = null;
 *   GRect segment;
 *   int subsample;
 *   final DjVuPage page;
 * 
 *   ... define constructors and other methods here ...
 * 
 *   public void run()
 *   { 
 *     boolean repeat;
 *     do
 *     {
 *       repeat=page.isDecoding();
 *       map=page.updateMap(map,segment,subsample); // get the map
 *       // We limit our wait to 200 ms, to query updates for new
 *       // segment or subsample values.  This also limits our wait
 *       // wait time when the page is updated before waitForCodec 
 *       // obtains a lock.
 *       page.waitForCodec(progressLock,200L);
 *     } while(repeat);
 *   }
 * }
 * </pre>
 */
public class DjVuPage
  extends DjVuObject
{
  //~ Static fields/initializers ---------------------------------------------

  /** This is the version of DjVu this code was written for. */
  public static const DJVUVERSION:int= 20;

  /** This is the oldest version of DjVu we can hope to decode. */
  public static const  DJVUVERSION_TOO_OLD:int= 15;

  /** This is the newest version of DjVu we should attempt to decode. */
  public static const DJVUVERSION_TOO_NEW:int= 22;
  
  /** The thread used for asynchronious decoding. */
  //protected static var queueThread:Thread= null;
  
  /** An array of vectors listing pages which need decoding. */
  protected static var queueVector:Array= (null, null);
  protected static var queueCount:int=0;
  public static const MAX_PRIORITY:int=1;
  public static const MIN_PRIORITY:int=0;

  /**
   * This number is incremented for each instance and used for logging.  Note
   * that there is no locking done when incrementing, so this number is not
   * guarenteed to be unique.
   */
  private static var idCount:int= 1;
  


  //~ Instance fields --------------------------------------------------------

  /** Lock used for accessing the annotation codec. */
  public var annoLock:Object= new String("anno");

  /** Lock used for accessing the background IWPixmap codec. */
  public var bgIWPixmapLock:Object= new String("bgIWPixmap");

  /** Lock to signal decoding is done. */
  public var doneLock:Object= new String("done");

  /** Lock used for accessing the foreground IWPixmap codec. */
  public var fgIWPixmapLock:Object= new String("fgIWPixmap");

  /** Lock used for accessing the foreground JB2Dict codec. */
  public var fgJb2DictLock:Object= new String("fgJb2Dict");

  /** Lock used for accessing the foreground JB2 codec. */
  public var fgJb2Lock:Object= new String("fgJb2");

  /** Lock used for accessing the foreground Palette codec. */
  public var fgPaletteLock:Object= new String("fgPalette");

  /** Lock used for accessing the info codec. */
  public var infoLock:Object= new String("info");

  /** Lock to signal any image update, for progressive rendering. */
  public var progressiveLock:Object= new String("progressive");

  /** Lock used for accessing the text codec. */
  public var textLock:Object= new String("text");

  /** The mimetype of this document. */
  public var mimetype:String= null;

  /** The shared dictionary data. */
  protected var sjbzChunk:CachedInputStream= null;

  /** Number incremented each time the image pixels are updated. */
  protected var progressiveCount:Number= 0;

  /** The creation time of this instance.  Used for logging purposes. */
  var startTime:Number= 0;//System.currentTimeMillis();

  /** Sets the data pool for this page. */
  private var pool:CachedInputStream=null;

  // The status string.
  private var status:String=null;

  /**
   * All the codec are stored in a hash table to make adding new codecs
   * easier.
   */
  private var codecTable:Hashtable= new Hashtable();

  /**
   * A reference to the foreground pixmap.  This will be regenerated if the
   * if the garbage collector removes it.
   */
  private var fgPixmapReference:Object= null;

  // Used to propigate change events
  //private var change:PropertyChangeSupport;

  /** In an exception is thrown in the decoding thread, we save it here. */
  //private var caughtException:Throwable= null;

  /** The URL for this page. */
  //protected var url:URL= null;

  /** True if a separate thread should be used for decoding. */
  private var asyncValue:Boolean= false;

  /** True if decode has been called. */
  private var decodeCalled:Boolean= false;

  /** True until the document has been decoded. */
  private var lockWait:Boolean= true;

  /** A unique number assign from idCount used for logging. */
  private var id:Number;
  
  private var priority:int=MAX_PRIORITY;

  //~ Constructors -----------------------------------------------------------

  /**
   * Creates a new DjVuPage object.
   */
  public function DjVuPage()
  {
    //change   = new PropertyChangeSupport(this);
    id       = idCount++;
    
   initStatic();
   
  }
  
  private static function initStatic() {
  	if(!queueVector) { 
    	queueVector=new Array(1+MAX_PRIORITY-MIN_PRIORITY);
    	for(var i:int=0;i<queueVector.length;) {
      		queueVector[i++]=new Vector();   		
    	}
   }
  }

  //~ Methods ----------------------------------------------------------------

  /**
   * Set the flag to allow or disallow asynchronous operations.
   *
   * @param value true if asynchronous operations should be used.
   */
  public final function setAsync(value:Boolean):void{
    asyncValue = value;
  }

  /**
   * Query if the asynchronous flag is set.
   *
   * @return true if the asynchronous operations are set.
   */
  public final function isAsync():Boolean{
    return asyncValue;
  }

  /**
   * Set the decode priority.  Must be called prior to decoding.
   *
   * @param value the decode priority
   */
  public final function setPriority(value:int):void{
    this.priority = Math.max(MIN_PRIORITY,Math.min(value,MAX_PRIORITY));
  }

  /**
   * Query if the asynchronous flag is set.  Must be called prior to decoding.
   *
   * @return true if the asynchronous operations are set.
   */
  public final function getPriority():int{
    return priority;
  }

  /**
   * Get the background pixmap.
   *
   * @param rect area to render
   * @param subsample rate to subsample
   * @param gamma color correction factor
   * @param retval an old pixmap to reuse rather than allocating more memory
   *
   * @return the pixmap of interest
   */
  public function getBgPixmap(
    rect:GRect,
    subsample:int,
    gamma:Number,
    retval:GPixmap):GPixmap{
    var pm:GPixmap= null;
    var info:DjVuInfo= getInfo();
    var width:int= (info == null)
      ? 0: info.width;
    var height:int= (info == null)
      ? 0: info.height;

    if((width <= 0) || (height <= 0) || (info == null))
    {
      return null;
    }

    var gamma_correction:Number= 1.0;

    if((gamma > 0) && (info != null))
    {
      gamma_correction = gamma / info.gamma;
    }

    if(gamma_correction < 0.10000000000000001)
    {
      gamma_correction = 0.10000000000000001;
    }
    else if(gamma_correction > 10)
    {
      gamma_correction = 10;
    }

    var bgIWPixmap:IWPixmap= getBgIWPixmap();

    if(bgIWPixmap != null)
    {
      var w:int= bgIWPixmap.getWidth();
      var h:int= bgIWPixmap.getHeight();

      if((w == 0) || (h == 0) || (width == 0) || (height == 0))
      {
        return null;
      }

      var red:int= compute_red(width, height, w, h);

      if((red < 1) || (red > 12))
      {
        return null;
      }

      if(subsample == red)
      {
        pm = bgIWPixmap.getPixmap(1, rect, retval);
      }
      else if(subsample == (2* red))
      {
        pm = bgIWPixmap.getPixmap(2, rect, retval);
      }
      else if(subsample == (4* red))
      {
        pm = bgIWPixmap.getPixmap(4, rect, retval);
      }
      else if(subsample == (8* red))
      {
        pm = bgIWPixmap.getPixmap(8, rect, retval);
      }
      else if((red * 4) == (subsample * 3))
      {
        var xrect:GRect= new GRect();
        xrect.xmin   = int(Math.floor(rect.xmin*4/3));
        xrect.ymin   = int(Math.floor(rect.ymin*4/3));
        xrect.xmax   = int(Math.ceil(rect.xmax*4/3));
        xrect.ymax   = int(Math.ceil(rect.ymax*4/3));
        var nrect:GRect=new GRect(0,0, rect.width(), rect.height());
        if(xrect.xmax > w)
        {
          xrect.xmax = w;
        }

        if(xrect.ymax > h)
        {
          xrect.ymax = h;
        }

        var ipm:GPixmap= bgIWPixmap.getPixmap(1, xrect, null);
        pm = (retval != null)
          ? retval
          : GPixmap.createGPixmap(this);
        pm.downsample43(ipm, nrect);
      }
      else
      {
        var po2:int= 16;

        while((po2 > 1) && (subsample < (po2 * red)))
        {
          po2 >>= 1;
        }

        var inw:int= ((w + po2) - 1) / po2;
        var inh:int= ((h + po2) - 1) / po2;
        var outw:int= ((width + subsample) - 1) / subsample;
        var outh:int= ((height + subsample) - 1) / subsample;
        var ps:GPixmapScaler= createGPixmapScaler(inw, inh, outw, outh);
        ps.setHorzRatio(red * po2, subsample);
        ps.setVertRatio(red * po2, subsample);

        var xrect:GRect= ps.getRequiredRect(rect);
        var ipm:GPixmap= bgIWPixmap.getPixmap(po2, xrect, null);
        pm = (retval != null)
          ? retval
          : GPixmap.createGPixmap(this);
        ps.scale(xrect, ipm, rect, pm);
      }

      if((pm != null) && (gamma_correction != 1.0))
      {
        pm.applyGammaCorrection(gamma_correction);

        for(var i:int= 0; i < 9; i++)
        {
          pm.applyGammaCorrection(gamma_correction);
        }
      }

      if(DjVuOptions.COLLECT_GARBAGE)
      {
 		java.util.gc.forceGC();
      }

      return pm;
    }
    else
    {
      return null;
    }
  }

  /**
   * Get the foreground bitmap.
   *
   * @param rect area of interest
   * @param subsample rate to subsample
   * @param align number of alignment pixels
   * @param retval an old image to fill rather than creating a new image
   *
   * @return the bitmap of interest
   */
  public final function getBitmap(
    rect:GRect,
    subsample:int,
    align:int,
    retval:GBitmap):GBitmap{
    return get_bitmap(rect, subsample, align, null);
  }

  /**
   * Query if this is a compound or photo DjVu page.
   *
   * @return true if color.
   */
  public function isColor():Boolean{
    return (is_legal_compound() || !is_legal_bilevel());
  }

  /**
   * Query if the thread is alive and processing.
   *
   * @return true if the thread is running and still processing.
   */
  public final function isDecoding():Boolean{
    return lockWait && decodeCalled;
  }

  /**
   * Query the Anno Codec for this page.
   *
   * @return Annotation for this page.
   */
  public function getAnno():Codec{
    return getCodec(annoLock);
  }

  /**
   * Query the background IWPixmap codec for this page.
   *
   * @return the background IWPixmap codec for this page.
   */
  public function getBgIWPixmap():IWPixmap{
    return (getCodec(bgIWPixmapLock) as IWPixmap);
  }

  /**
   * Query the named Codec for this page.
   *
   * @param nameLock DOCUMENT ME!
   *
   * @return the named Codec for this page.
   */
  public function getCodec(nameLock:Object):Codec{
      return codecTable.get(nameLock) as Codec;    
  }

  /**
   * Create a new map.  The rectangle of interest and subsample rate are
   * looked up from the old map, and stored in the new map.  A null may be
   * returned if the subsample rate is not legal or there is no valid image.
   *
   * @param segment The bounding rectangle of the subsampled segment.
   * @param subsample The subsample rate.
   * @param retval an old image to fill rather than creating a new image
   *
   * @return The newly created image map.
   */
  public final function getMap(
    segment:GRect,
    subsample:int,
    retval:GMap):GMap{
    var count:Number= progressiveCount;
    
    if(isColor()) {
    	var subretval:GMap = null;
    	if(retval instanceof GPixmap) {
    		subretval = retval;
    	}
    	
    	retval = GMap(getPixmap(
        segment,
        subsample,
        0.0,
        subretval as GPixmap));
    } else {
    	var subretval:GMap = null;
    	if(retval instanceof GBitmap) {
    		subretval = retval;
    	}
    	
    	retval = GMap(getBitmap(
        segment,
        subsample,
        1,
        subretval as GBitmap));
    }

    if(retval != null)
    {
      retval.properties.put(progressiveLock, count);
      retval.properties.put(
        "rect",
        segment.clone());
      retval.properties.put(
        "subsample",
        new Integer(subsample));
    }

    return retval;
  }

  /**
   * Query the foreground IWPixmap for this page.
   *
   * @return The foreground IWPixmap for this page.
   */
  public function getFgIWPixmap():IWPixmap{
    // There is no need to synchronize since we won't access data which could be updated.
    return codecTable.get(fgIWPixmapLock) as IWPixmap;
  }

  /**
   * Query the foreground Jb2 codec for this page.
   *
   * @return the foreground Jb2 codec for this page.
   */
  public function getFgJb2():JB2Image{
    // There is no need to synchronize since we won't access data which could be updated.
    return codecTable.get(fgJb2Lock) as JB2Image;
  }

  /**
   * Query the foreground Jb2 Dict codec for this page.
   *
   * @return the foreground Jb2 Dict codec for this page.
   */
  public function getFgJb2Dict():JB2Dict{
    // There is no need to synchronize since we won't access data which could be updated.
    return codecTable.get(fgJb2DictLock) as JB2Dict;
  }

  /**
   * Query the foreground palette codec for this page.
   *
   * @return foreground palette codec for this page.
   */
  public function getFgPalette():Palette{
    // There is no need to synchronize since we won't access data which could be updated.
    return codecTable.get(fgPaletteLock) as Palette;
  }

  /**
   * Query the foreground pixmap for this page.
   *
   * @return The foreground pixmap for this page.
   */
  public function getFgPixmap():GPixmap{
    var fgPixmap:GPixmap= GPixmap(getFromReference(fgPixmapReference));

    if(fgPixmap == null)
    {
      var fgIWPixmap:IWPixmap= getFgIWPixmap();

      if(fgIWPixmap != null)
      {
        //synchronized(fgIWPixmapLock)
        {
          fgPixmap = GPixmap(getFromReference(fgPixmapReference));

          if(fgPixmap == null)
          {
            fgPixmap            = fgIWPixmap.getPixmap();
            fgPixmapReference   = createSoftReference(fgPixmap, fgPixmap);
          }
        }
      }
    }

    return fgPixmap;
  }

  /**
   * Query the DjVuInfo for this page.
   *
   * @return DjVuInfo for this page.
   */
  public function getInfo():DjVuInfo{
    // There is no need to synchronize since we won't access data which could be updated.
    return codecTable.get(infoLock) as DjVuInfo;
  }

  /**
   * Query the DjVuInfo for this page.
   *
   * @return DjVuInfo for this page.
   */
  public function getInfoWait():DjVuInfo{
    return waitForCodec(infoLock, 0) as DjVuInfo;
  }

  /**
   * Query the progressive count.
   *
   * @return the named Codec for this page.
   */
  public function getProgressiveCount():int{
    return int(progressiveCount);
  }

  /**
   * Query the hidden text Codec for this page.
   *
   * @return Hidden text for this page.
   */
  public function getText():Codec{
    // There is no need to synchronize since we won't access data which could be updated.
    return codecTable.get(textLock) as Codec;
  }

  /**
   * Add a listener for property change events.
   *
   * @param listener to add
   */
  public function addPropertyChangeListener(
    listener:PropertyChangeListener):void{
    //change.addPropertyChangeListener(listener);
  }

  /**
   * Compute the subsample rate used.
   *
   * @param w full size width
   * @param h fill size height
   * @param rw reduced width
   * @param rh reduced height
   *
   * @return subsample rate, or 16
   */
  public static function compute_red(
    w:int,
    h:int,
    rw:int,
    rh:int):int{
    for(var red:int= 1; red < 16; red++)
    {
      if(((((w + red) - 1) / red) == rw) && ((((h + red) - 1) / red) == rh))
      {
        return red;
      }
    }

    return 16;
  }

  /**
   * Creates an instance of DjVuPage with the options interherited from the
   * specified reference.
   *
   * @param ref Object to interherit DjVuOptions from.
   *
   * @return a new instance of DjVuPage.
   */
  public static function createDjVuPage(ref:DjVuInterface):DjVuPage{
    var options:DjVuOptions= ref.getDjVuOptions();

    return create(
      options,
      options.getDjVuPageClass(),
      "com.lizardtech.djvu.DjVuPage") as DjVuPage;
  }

  /**
   * Called to create an instance of GPixmapScaler
   *
   * @param inw Source image width.
   * @param inh Source image height.
   * @param outw Destination image width.
   * @param outh Destination image height.
   *
   * @return DOCUMENT ME!
   */
  public function createGPixmapScaler(
    inw:int,
    inh:int,
    outw:int,
    outh:int):GPixmapScaler{
    return new GPixmapScaler(inw, inh, outw, outh);
  }


  /**
   * Decode the specified CachedInputStream.
   * 
   * @param pool CachedInputStream to decode.
   * 
   * @throws IOException if an error occures.
   * @throws IllegalStateException DOCUMENT ME!
   */

  /**
   * Query if the specified codec is available.
   *
   * @param nameLock DOCUMENT ME!
   *
   * @return true if the specified codec is available.
   */
  public function hasCodec(nameLock:Object):Boolean{
    return (codecTable.get(nameLock) instanceof Codec);
  }

  /**
   * Test if this is a photo image.
   *
   * @return true if a photo image
   */
  public function is_legal_photo():Boolean{
    var info:DjVuInfo= getInfo();

    if(info == null)
    {
      return false;
    }

    var width:int= info.width;
    var height:int= info.height;

    if((width <= 0) || (height <= 0))
    {
      return false;
    }

    return hasCodec(bgIWPixmapLock) && !hasCodec(fgJb2Lock)
    && !hasCodec(fgIWPixmapLock);
  }

  /**
   * Remove a listener for PropertyChangeEvent.
   *
   * @param listener to remove
   */
  public function removePropertyChangeListener(
    listener:PropertyChangeListener):void{
    //change.removePropertyChangeListener(listener);
  }

  /**
   * Update an image map based on an existing one.  If the existing map is
   * null  or anything has been updated, a new image map will be fetched.
   * Otherwise, the existing image will be returned.  A null may be returned
   * if the subsample  rate is not legal, or no pixels are available.
   *
   * @param map The existing map.
   * @param segment The bounding rectangle of the subsampled segment.
   * @param subsample The subsample rate.
   *
   * @return The newly created image map.
   */
  public final function updateMap(
    map:GMap,
    segment:GRect,
    subsample:int):GMap{
    if(map != null)
    {
      var subsampleO:Object= map.properties.get("subsample");

      if(
        (subsampleO instanceof Number)
        && (int(subsampleO) == subsample)
        && progressiveCount == map.properties.get(progressiveLock)
        && segment == map.properties.get("rect"))
      {
        return map;
      }
    }

    return getMap(segment, subsample, null);
  }

  /**
   * Get the pixmap for the desired region.
   *
   * @param rect area of interest
   * @param subsample rate to subsample
   * @param gamma color correction factor
   * @param retval an old image to use instead of creating a new one
   *
   * @return the pixmap of interest
   */
  public function getPixmap(
    rect:GRect,
    subsample:int,
    gamma:Number,
    retval:GPixmap):GPixmap{
    if(rect.isEmpty())
    {
      return (retval == null)
      ? (GPixmap.createGPixmap(this))
      : retval.init(0, 0, null);
    }

    var bg:GPixmap=getBgPixmap(rect, subsample, gamma, retval);
    if(hasCodec(fgJb2Lock))
    {
      if(bg == null)
      {
          bg=(retval == null)?GPixmap.createGPixmap(this):retval;
          bg.init(
           rect.height(), 
           rect.width(), GPixel.WHITE);
      }
      if(stencil(bg, rect, subsample, gamma))
      {
        retval=bg;
      }
    }
    else
    {
        retval=bg;
    }
    return retval;
  }

  /**
   * Get a bitmap for the specifed region
   *
   * @param rect area of interest
   * @param subsample subsample rate
   * @param align border alignment
   * @param components a list of components
   *
   * @return the newly created image
   */
  public function get_bitmap(
    rect:GRect,
    subsample:int,
    align:int,
    components:Vector):GBitmap{
    if(rect.isEmpty())
    {
      return GBitmap.createGBitmap(this);
    }

    var info:DjVuInfo= getInfo();

    if(info != null)
    {
      var width:int= info.width;
      var height:int= info.height;

      var fgJb2:JB2Image= getFgJb2();

      if(
        (width != 0)
        && (height != 0)
        && (fgJb2 != null)
        && (fgJb2.width == width)
        && (fgJb2.height == height))
      {
        return fgJb2.get_bitmap(rect, subsample, align, 0, components);
      }
    }

    return null;
  }

  /**
   * Query if this is a bitonal image.
   *
   * @return true if bitonal
   */
  public function is_legal_bilevel():Boolean{
    var info:DjVuInfo= getInfo();

    if(info == null)
    {
      return false;
    }

    var width:int= info.width;
    var height:int= info.height;

    if((width <= 0) || (height <= 0))
    {
      return false;
    }

    var fgJb2:JB2Image= getFgJb2();

    if((fgJb2 == null) || (fgJb2.width != width) || (fgJb2.height != height))
    {
      return false;
    }

    return !(hasCodec(bgIWPixmapLock) || hasCodec(fgIWPixmapLock)
    || hasCodec(fgPaletteLock));
  }

  /**
   * Query if this is a compound image.
   *
   * @return true if a compound image
   */
  public function is_legal_compound():Boolean{
    var info:DjVuInfo= getInfo();

    if(info == null)
    {
      return false;
    }

    var width:int= info.width;
    var height:int= info.height;

    if((width <= 0) || (height <= 0))
    {
      return false;
    }

    var fgJb2:JB2Image= getFgJb2();

    if((fgJb2 == null) || (fgJb2.width != width) || (fgJb2.height != height))
    {
      return false;
    }

    // There is no need to synchronize since we won't access data which could be updated.
    var bgIWPixmap:IWPixmap= IWPixmap(codecTable.get(bgIWPixmapLock));
    var bgred:int= 0;

    if(bgIWPixmap != null)
    {
      bgred =
        compute_red(
          width,
          height,
          bgIWPixmap.getWidth(),
          bgIWPixmap.getHeight());
    }

    if((bgred < 1) || (bgred > 12))
    {
      return false;
    }

    var fgred:int= 0;

    if(hasCodec(fgIWPixmapLock))
    {
      var fgPixmap:GPixmap= getFgPixmap();
      fgred = compute_red(
          width,
          height,
          fgPixmap.columns(),
          fgPixmap.rows());
    }

    return ((fgred >= 1) && (fgred <= 12));
  }

  /**
   * This thread processes the asynchronous queue.  Only one page is decoded
   * at a time, with the most recient requested page processed  first.
   */
   
   /*
  public function run():void{
    var current:Thread= Thread.currentThread();
    var priority:int=MIN_PRIORITY;
    logError("queue + "+this);
    while(queueThread == current)
    {
      var page:DjVuPage= null;

      synchronized(queueVector)
      {
        if(queueCount == 0)
        {
          try
          {
            queueVector.wait(5000);
          }
          catch(ignore:*) {
          	// nothing
          }
          if(queueCount == 0)
          {
            queueThread = null;

            break;
          }
        }
        priority=queueVector.length;
        while(priority>0)
        {
            var queue:Vector=queueVector[--priority];
            if(queue.size() > 0)
            {
              var ref:Object=queue.lastElement();
              page = DjVuPage(getFromReference(ref));
              queue.removeElementAt(queue.size() - 1);
              queueCount--;
              if(page != null)
              {
                if(priority < MAX_PRIORITY)
                {
                  var q:Vector=queueVector[priority+1];
                  q.addElement(ref);
                  queueCount++;
                  page=null;
                  try { queueVector.wait(200); } catch(ignored:*) {}
                }
                break;
              }
            }
        }
      }

      if(page != null)
      {
        try
        {
//          logError("queue decode + "+page);
          page.decode();
        }
        catch(exp:*)
        {
          printStackTrace(exp);
        }
      }
    }
//    logError("queue - "+this);
  }
*/

  /**
   * Set the status string and fire a property change event "status".
   *
   * @param status new status string
   */
  public function setStatus(status:String):void{
    var s:String=this.status;
    this.status=status;
   // change.firePropertyChange("status", s, status);
  }

  /**
   * Query the status string.
   *
   * @return the status string
   */
  public function getStatus():String{
      return status;
  }

  /**
   * Create an image by stenciling the foreground onto the background.
   *
   * @param pm the background image to stencil
   * @param rect the area of the page being stenciled
   * @param subsample the subsample rate of the area being stenciled
   * @param gamma the color correction factor
   *
   * @return true if successfull
   */
  public function stencil(
    pm:GPixmap,
    rect:GRect,
    subsample:int,
    gamma:Number):Boolean{
    var info:DjVuInfo= getInfo();

    if(info == null)
    {
      return false;
    }

    var width:int= info.width;
    var height:int= info.height;

    if((width <= 0) || (height <= 0))
    {
      return false;
    }

    var gamma_correction:Number= 1.0;

    if(gamma > 0.0)
    {
      gamma_correction = gamma / info.gamma;
    }

    if(gamma_correction < 0.10000000000000001)
    {
      gamma_correction = 0.10000000000000001;
    }
    else if(gamma_correction > 10)
    {
      gamma_correction = 10;
    }

    var fgJb2:JB2Image= getFgJb2();

    if(fgJb2 != null)
    {
      var fgPalette:Palette= getFgPalette();

      if(fgPalette != null)
      {
        var components:Vector= new Vector();
        var bm:GBitmap= get_bitmap(rect, subsample, 1, components);

        if(fgJb2.get_blit_count() != fgPalette.colordata.length)
        {
          pm.attenuate(bm, 0, 0);

          return false;
        }

        var colors:GPixmap=
          GPixmap.createGPixmap(this).init(
            1,
            fgPalette.size(),
            null);
        var color:GPixelReference= colors.createGPixelReference(0);

        for(var i:int= 0; i < colors.columns(); color.incOffset())
        {
          fgPalette.index_to_color(i++, color);
        }

        colors.applyGammaCorrection(gamma_correction);

        var compset:Vector= new Vector();

        while(components.size() > 0)
        {
          var lastx:int= 0;
          var colorindex:int=
            fgPalette.colordata[int(components.elementAt(0))];
          var comprect:GRect= new GRect();
          compset.setSize(0);

          for(var pos:int= 0; pos < components.size();)
          {
            var blitno:int=
              int(components.elementAt(pos));
            var pblit:JB2Blit= fgJb2.get_blit(blitno);

            if(pblit.ileft < lastx)
            {
              break;
            }

            lastx = pblit.ileft;

            if(fgPalette.colordata[blitno] == colorindex)
            {
              var pshape:JB2Shape= fgJb2.get_shape(pblit.ishapeno);
              var xrect:GRect=
                new GRect(
                  pblit.ileft,
                  pblit.ibottom,
                  pshape.getGBitmap().columns(),
                  pshape.getGBitmap().rows());
              comprect.recthull(comprect, xrect);
              compset.addElement(components.elementAt(pos));
              components.removeElementAt(pos);
            }
            else
            {
              pos++;
            }
          }

          comprect.xmin /= subsample;
          comprect.ymin /= subsample;
          comprect.xmax   = ((comprect.xmax + subsample) - 1) / subsample;
          comprect.ymax   = ((comprect.ymax + subsample) - 1) / subsample;
          comprect.intersect(comprect, rect);

          if(comprect.isEmpty())
          {
            continue;
          }

          //        bm   = getBitmap(comprect, subsample, 1);
          bm = GBitmap.createGBitmap(this);
          bm.init(
            comprect.height(),
            comprect.width(),
            0);
          bm.setGrays(1+ (subsample * subsample));

          var rxmin:int= comprect.xmin * subsample;
          var rymin:int= comprect.ymin * subsample;

          for(var pos:int= 0; pos < compset.size(); ++pos)
          {
            var blitno:int=
              int(compset.elementAt(pos));
            var pblit:JB2Blit= fgJb2.get_blit(blitno);
            var pshape:JB2Shape= fgJb2.get_shape(pblit.ishapeno);
            bm.blit(
              pshape.getGBitmap(),
              pblit.ileft - rxmin,
              pblit.ibottom - rymin,
              subsample);
          }

          color.setOffset(colorindex);
          pm.blit(
            bm,
            comprect.xmin - rect.xmin,
            comprect.ymin - rect.ymin,
            color);
        }

        return true;
      }

      // Three layer model.
      var fgIWPixmap:IWPixmap= getFgIWPixmap();

      if(fgIWPixmap != null)
      {
        var bm:GBitmap= getBitmap(rect, subsample, 1, null);

        if((bm != null) && (pm != null))
        {
          var fgPixmap:GPixmap= getFgPixmap();
          var w:int= fgPixmap.columns();
          var h:int= fgPixmap.rows();
          var red:int= compute_red(width, height, w, h);

//          if((red < 1) || (red > 12))
          if((red < 1) || (red > 16))
          {
            return false;
          }
//
//          int supersample = (red <= subsample)
//            ? 1
//            : (red / subsample);
//          int wantedred = supersample * subsample;
//
//          if(red == wantedred)
//          {
//            pm.stencil(bm, fgPixmap, supersample, rect, gamma_correction);
//
//            return 1;
//          }
          pm.stencil(bm, fgPixmap, red, subsample, rect, gamma_correction);
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Query the named Codec for this page.  If the Codec is not available, and
   * decoding is in progress then wait for it.
   *
   * @param nameLock the name lock of the codec to wait for
   * @param maxTime the maximum amount of time to wait for the specified
   *        codec
   *
   * @return the named Codec for this page.
   */
  public function waitForCodec(
    nameLock:Object,
    maxTime:Number):Codec{
    var active:Boolean;
    var retval:Codec;

//    logError("wait + "+this+" "+nameLock);
    do
    {
      //synchronized(nameLock)
      {
        active   = isDecoding();
        retval   = Codec(codecTable.get(nameLock));

        if(active && (retval == null))
        {
          active = isDecoding();

          if(active)
          {
            // We wait a maximum of 5 seconds instead of forever to avoid deadlocks...
            try
            {
              nameLock.wait((maxTime <= 0)
                ? 5000: maxTime);
            }
            catch(ignored:*) {}

            retval = Codec(codecTable.get(nameLock));
          }
        }
      }
    }
    while(
      active
      && (retval == null)
      && (maxTime <= 0)
      && (nameLock != progressiveLock));

//    logError("wait - "+this+" "+nameLock);
    return retval;
  }

  /**
   * Called after processing each chunk to log progress and optionally run
   * the garbage collector.
   *
   * @param chkid name of chunk processed
   */
  protected function clean(chkid:String):void{
  	/*
    var run:Runtime= Runtime.getRuntime();
    var used:Number= run.totalMemory() - run.freeMemory();
    var t:Number= System.currentTimeMillis() - startTime;
    var d:String= "000" + t;
    verbose(
      id + ". chkid=" + chkid + ",memory=" + used + " time=" + (t / 1000)
      + "." + d.substring(d.length() - 3));
      */
	trace("chunk: " + chkid);

    if(DjVuOptions.COLLECT_GARBAGE)
    {
      java.util.gc.forceGC();
    }
  }

  /**
   * Called to decode a chunk.
   *
   * @param iff stream being processed
   * @param isInclude true if this is an include file stream
   *
   * @throws IOException if an error occurs
   * @throws IllegalStateException if an error occurs
   */
  protected function decodeChunk(
    iff:CachedInputStream,
    isInclude:Boolean):void
  {
    var chkid:String=iff.getName();
    try
    {
      if(chkid == "Djbz")
      {
        addCodecChunk(
          fgJb2DictLock,
          JB2Dict.createJB2Dict(this),
          iff);

        if(sjbzChunk != null)
        {
          parseSjbz(CachedInputStream(sjbzChunk.clone()));
        }
      }
      else if(chkid == "ANTa" || chkid == "ANTz")
      {
        //synchronized(annoLock)
        {
          var anno:Codec= getCodec(annoLock);

          if(anno == null)
          {
            anno = getDjVuOptions().createDjVuAnno();
          }

          addCodecChunk(annoLock, anno, iff);
        }
      }
      else if(!isInclude)
      {
        if(chkid == "INFO")
        {
          throw new Error(
            "DjVu Decoder: Corrupted file (Duplicate INFO chunk)");
        }
        else if(chkid == "INCL")
        {
//          ByteArrayOutputStream bout   = new ByteArrayOutputStream();
//          byte[]                buffer = new byte[1024];
//
//          for(int i; (i = iff.read(buffer)) > 0;)
//          {
//            bout.write(buffer, 0, i);
//          }
//
//         decodeInclude(createCachedInputStream(new String(bout.toByteArray())));
         decodeInclude(createCachedInputStream(iff.readFullyUTF()));
        }
        else if(chkid == "FGbz")
        {
          if(hasCodec(fgIWPixmapLock))
          {
            throw new Error("Duplicate foreground");
          }

          addCodecChunk(
            fgPaletteLock,
            Palette.createPalette(this),
            iff);
        }
        else if(chkid == "TXTa" || chkid == "TXTz")
        {
          addCodecChunk(
            textLock,
            getDjVuOptions().createDjVuText(),
            iff);
        }
        else if(chkid == "Sjbz")
        {
          if(this.sjbzChunk != null)
          {
            throw new Error("Duplicate Sjbz chunk");
          }

//          final CachedInputStream sjbzChunk = iff.getCachedInputStream();

          try
          {
            iff.mark(Integer.MAX_VALUE);
            parseSjbz(iff);
          }
          catch(exp:*)
          {
//            if(hasCodec(fgJb2DictLock) || (sjbzChunk == null))
            if(hasCodec(fgJb2DictLock))
            {
              throw exp;
            }
            iff.reset();
            this.sjbzChunk = iff;
          }
        }
        else if(chkid == "BG44")
        {
//bcr          if(getInfo().width > 600){verbose("bcr extra wait");try {Thread.sleep(5000L);}catch(final Throwable ignored) {}}
          //synchronized(bgIWPixmapLock)
          {
            var bgIWPixmap:Codec= getCodec(bgIWPixmapLock);

            if(bgIWPixmap == null)
            {
              bgIWPixmap = IWPixmap.createIWPixmap(this);
            }

            addCodecChunk(bgIWPixmapLock, bgIWPixmap, iff);
          }
        }
        else if(chkid == "FG44")
        {
          if(hasCodec(fgPaletteLock))
          {
            throw new Error(
              "DjVu Decoder: Corrupted data (Duplicate foreground layer)");
          }

          addCodecChunk(
            fgIWPixmapLock,
            IWPixmap.createIWPixmap(this),
            iff);
        }
        else if(chkid == "BG2k")
        {
          if(hasCodec(bgIWPixmapLock))
          {
            throw new Error(
              "DjVu Decoder: Corrupted data (Duplicate background layer)");
          }
        }
        else if(chkid == "FG2k")
        {
          if(hasCodec(fgIWPixmapLock) || hasCodec(fgPaletteLock))
          {
            throw new Error(
              "DjVu Decoder: Corrupted data (Duplicate foreground layer)");
          }
        }
        else if(chkid == "Smmr")
        {
          if(hasCodec(fgJb2Lock))
          {
            throw new Error(
              "DjVu Decoder: Corrupted data (Duplicate background layer)");
          }
        }
        else if(chkid == "BGjp")
        {
          if(hasCodec(bgIWPixmapLock))
          {
            throw new Error (
              "DjVu Decoder: Corrupted data (Duplicate background layer)");
          }
        }
        else if(chkid == "FGjp" && hasCodec(fgIWPixmapLock))
        {
          throw new Error(
            "DjVu Decoder: Corrupted data (Duplicate foreground layer)");
        }
      }
    }
    finally
    {
//      iff.chunkClose();
      clean(chkid);
    }
  }

  /**
   * Called to decode chunks.
   *
   * @param iff an Enumeration of CachedInputStream's to decode
   * @param isInclude true if this is an include file stream
   *
   * @throws IOException if an error occurs
   */
  protected function decodeChunks(
    iff:Enumeration,
    isInclude:Boolean):void
  {
    if((iff == null)||!iff.hasMoreElements())
    {
      return;
    }

    if(!hasCodec(infoLock))
    {

      var chunk:CachedInputStream=CachedInputStream(iff.nextElement());
      if(!("INFO" == chunk.getName()))
      {
        throw new Error(
          "DjVuDecoder:: Corrupted file (Does not start with INFO chunk)");
      }

      addCodecChunk(
        infoLock,
        DjVuInfo.createDjVuInfo(this),
        chunk);
      clean(chunk.getName());
    }
    while(iff.hasMoreElements())
    {
      decodeChunk(
        CachedInputStream(iff.nextElement()),
        isInclude);
    }
  }

  /**
   * Called to decode an include chunk.
   *
   * @param pool chunk to be read
   *
   * @throws IOException if an error occurs
   * @throws IllegalStateException if an error occurs
   */
  protected function decodeInclude(pool:CachedInputStream):void
  {
    var iff:Enumeration= pool.getIFFChunks();
      if((iff == null)||!iff.hasMoreElements())
      {
        throw new IOException("EOF");
      }
      var formStream:CachedInputStream=iff.nextElement() as CachedInputStream;
      var formIff:Enumeration=formStream.getIFFChunks();
      if((formIff != null)&&"FORM:DJVI" == formStream.getName())
      {
        decodeChunks(formIff, true);
      }
      else
      {
        throw new Error(
          "DejaVu decoder: a DJVI include was expected");
      }
  }

  /**
   * Called to parse jb2 data.
   *
   * @param input stream to parse
   *
   * @throws IOException if an error occurs
   * @throws IllegalStateException if an error occurs
   */
  protected function parseSjbz(input:InputStream):void
  {
    sjbzChunk = null;

    var fgJb2:JB2Image= JB2Image.createJB2Image(this);

    var info:DjVuInfo= getInfo();

    if((info != null) && (info.version < 19))
    {
      fgJb2.reproduce_old_bug = true;
    }

    fgJb2.decode(
      input,
      getFgJb2Dict());

    if(setCodec(fgJb2Lock, fgJb2) != null)
    {
      throw new Error(
        "DjVu Decoder: Corrupted data (Duplicate FGxx chunk)");
    }
  }

  /**
   * Called to create a CachedInputStream of the given id.
   * 
   * @param id name of the CachedInputStream to create
   * 
   * @return the newly created CachedInputStream
   * 
   * @throws IOException if an error occurs
   */
  function createCachedInputStream(id:String):CachedInputStream
  {
    //return CachedInputStream.createCachedInputStream(this).init(new URL(url, id), false);
    return null;
  }

  /**
   * Set the named Codec for this page.
   *
   * @param nameLock Name of the codec to set.
   * @param codec Named codec for for this page.
   *
   * @return Old codec of this name for this page.
   */
  private function setCodec(
    nameLock:Object,
    codec:Codec):Codec{
    var retval:Codec;
    var name:String= String(nameLock);

    //synchronized(nameLock)
    {
      retval =
        Codec(((codec == null)
        ? codecTable.remove(nameLock)
        : codecTable.put(nameLock, codec)));
      //nameLock.notifyAll();

      if(retval != codec)
      {
       // change.firePropertyChange(name, retval, codec);
      }
      else if(nameLock == annoLock)
      {
        //change.firePropertyChange(String(annoLock), null, codec);
      }
    }

    if((codec != null)&&codec.isImageData())
    {
      //synchronized(progressiveLock)
      {
        var count:Number= progressiveCount;
        progressiveCount = new int(count + 1);
        //progressiveLock.notifyAll();
        //change.firePropertyChange(
        //  String(progressiveLock),
        //  count,
        //  progressiveCount);
      }
    }
    return retval;
  }

  // Decode the specified codec from the datapool.
  private function addCodecChunk(
    nameLock:Object,
    codec:Codec,
    pool:CachedInputStream):void
  {
    if(codec != null)
    {
      codec.decode(pool);

      var old:Codec= setCodec(nameLock, codec);

      if((old != null) && (old != codec))
      {
        throw new Error("Duplicate " + nameLock);
      }
    }
  }

  /**
   * Decode the document from the already opened datapool.  Normally this
   * method is called the queue.
   *
   * @throws IOException if an error occurs
   * @throws IllegalStateException if an error occurs
   */
   
   
   public function decode(pool:CachedInputStream=null):void {
   	 if(pool) {
	      if(getProgressiveCount() != 0)
	      {
	        throw new Error(
	          flash.utils.getQualifiedClassName(this) + " decode already called.");
	      }
	
	      progressiveCount   = 1;
	      decodeCalled       = true;
	    
	
	      this.pool = pool;
	
	      decode();
	
	  
	  } else {
	
	    try
	    {
	      //startTime = System.currentTimeMillis();
	      var url:URL=null; //this.url;
	  
	      var pool:CachedInputStream= this.pool;
	      this.pool = null;
	
	      var iff:Enumeration= pool.getIFFChunks();
	      
	      trace("enum");
	      try
	      {
	      	/*
	        if((iff == null)||!iff.hasMoreElements())
	        {
	          throw new IOException("EOF");
	        }
	        */
	        
	        var formStream:CachedInputStream=iff.nextElement() as CachedInputStream;
	        trace("stream name: " + formStream.getName());
	        if("FORM:DJVU" == formStream.getName())
	        {
	          mimetype = "image/djvu";
	          var subChunks:Enumeration =  formStream.getIFFChunks();
	          decodeChunks(subChunks, false);
	          if(sjbzChunk != null)
	          {
	            parseSjbz(CachedInputStream(sjbzChunk.clone()));
	            clean("Sjbz");
	          }
	          var bgIWPixmap:IWPixmap= getBgIWPixmap();
	          if(bgIWPixmap != null)
	          {
	            bgIWPixmap.close_codec();
	          }
	          var info:DjVuInfo= getInfo();
	          if(info == null)
	          {
	            throw new Error(
	              "DjVu Decoder: Corrupted data (Missing INFO chunk)");
	          }
	        }
	        else if("FORM:PM44" == formStream.getName()
	          || "FORM:BM44" == formStream.getName())
	        {
	          mimetype = "image/iw44";
	          var img44:IWPixmap= null;
	          var formIff:Enumeration=formStream.getIFFChunks();
	          while((formIff != null)&&formIff.hasMoreElements())
	          {
	            var chunk:CachedInputStream=CachedInputStream(formIff.nextElement());
	            if("PM44" == chunk.getName() || "BM44" == chunk.getName())
	            {
	              if(img44 != null)
	              {
	                img44 = IWPixmap.createIWPixmap(this);
	                img44.decode(chunk);
	                var info:DjVuInfo= DjVuInfo.createDjVuInfo(this);
	                info.width    = img44.getWidth();
	                info.height   = img44.getHeight();
	                info.dpi      = 100;
	                setCodec(infoLock, info);
	                setCodec(bgIWPixmapLock, img44);
	              }
	              else
	              {
	                img44.decode(chunk);
	              }
	            }
	            else if("ANTa" == chunk.getName() || "ANTz" == chunk.getName())
	            {
	              //synchronized(annoLock)
	              {
	                var anno:Codec= getCodec(annoLock);
	
	                if(anno == null)
	                {
	                  anno = getDjVuOptions().createDjVuAnno();
	                }
	                addCodecChunk(annoLock, anno, chunk);
	              }
	            }
	          }
	
	          if(!hasCodec(infoLock))
	          {
	            throw new Error(
	              "DjVu Decoder: Corrupted data (Missing IW44 data chunks)");
	          }
	        }
	        else
	        {
	          throw new Error(
	            "DejaVu decoder: a DJVU or IW44 image was expected");
	        }
	      }
	      finally
	      {
	        if(url != null)
	        {
	          setStatus("decoded "+url); 
	        }
	      }
	    }
	    catch(exp:*)
	    {
	      //caughtException = exp;
	      printStackTrace(exp);
	    }
	    lockWait = false;
	
	    try
	    {
	//        logError("0. finish "+this);
	      var lockArray:Array=
	      [
	        infoLock, fgIWPixmapLock, annoLock, textLock, bgIWPixmapLock,
	        fgJb2Lock, fgPaletteLock, fgJb2DictLock, progressiveLock, doneLock
	      ];
	
		  /*
	      for(var i:int= 0; i < lockArray.length; i++)
	      {
	//          logError(i+". finish "+this);
	        //synchronized(lockArray[i])
	        {
	          lockArray[i].notifyAll();
	        }
	      }
	      */
	//        logError("x. finish "+this);
	      //change.firePropertyChange(
	      //  doneLock.toString(),
	      //  null,
	      //  this);
	
	//        logError("xx. finish "+this);
	    }
	    catch(exp:*)
	    {
	      printStackTrace(exp);
	    }
	  }
  }
}}